package de.fmaul.common.collect;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.common.base.Function;
import com.google.common.collect.ForwardingList;
import com.google.common.collect.Lists;

/**
 * This class provides a wrapper around a {@link List} and additionally provides
 * a unique index over the lists elements to provide constant access times like
 * a Map or HashMap would. To achieve this a HashMap is automatically maintained
 * whenever the list is altered.
 * 
 * The Index is created by an index function with has to be provided and enables
 * the index to operate on any field so the index is independent of the
 * {@link #equals(Object)} and {@link #hashCode()} implementations.
 * 
 * @author Florian Maul
 * @param <E>
 *            the type of the list elements.
 * @param <K>
 *            the type that will be used as index keys.
 */
public class UniqueIndexedList<E, K> extends ForwardingList<E> {

    private final List<E> delegateList;
    private final Function<E, K> indexFunction;
    private final Map<K, E> indexMap = new HashMap<K, E>();
    private final Map<K, E> readOnlyIndexMap = Collections
            .unmodifiableMap(indexMap);

    /**
     * Creates a new {@link UniqueIndexedList} for a list and an index function.
     * 
     * @param delegate
     *            The list all calls are forwarded to. After creating the
     *            UniqueIndexedList this should not be used directly anymore.
     * @param indexFunction
     *            The function which converts list elements in index keys.
     */
    public UniqueIndexedList(List<E> delegate, Function<E, K> indexFunction) {
        delegateList = delegate;
        this.indexFunction = indexFunction;
        rebuildIndex();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingList#delegate()
     */
    @Override
    protected List<E> delegate() {
        return delegateList;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingCollection#add(java.lang.Object)
     */
    @Override
    public boolean add(E e) {
        boolean changed = super.add(e);
        if (changed) {
            indexElement(e);
        }
        return changed;
    };

    /**
     * Puts the list element e into the index.
     * 
     * @param e
     *            An element of the list.
     */
    private void indexElement(final E e) {
        indexMap.put(indexFunction.apply(e), e);
    }

    /**
     * Puts all elements of a collection into the index.
     * 
     * @param collection
     *            A collection of list elements.
     */
    private void indexElements(final Collection<? extends E> collection) {
        for (E element : collection) {
            indexElement(element);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.google.common.collect.ForwardingCollection#addAll(java.util.Collection
     * )
     */
    @Override
    public boolean addAll(Collection<? extends E> collection) {
        boolean changed = super.addAll(collection);
        if (changed) {
            indexElements(collection);
        }
        return changed;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingList#add(int, java.lang.Object)
     */
    public void add(int index, E element) {
        super.add(index, element);
        indexElement(element);
    };

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingList#addAll(int,
     * java.util.Collection)
     */
    @Override
    public boolean addAll(int index, Collection<? extends E> elements) {
        boolean changed = super.addAll(index, elements);
        if (changed) {
            indexElements(elements);
        }
        return changed;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingList#set(int, java.lang.Object)
     */
    @Override
    public E set(int index, E element) {
        E oldElement = super.set(index, element);
        if (oldElement != null && oldElement != element) {
            removeIndex(oldElement);
            indexElement(element);
        }
        return oldElement;
    };

    /**
     * Removes an element from the index.
     * 
     * @param element
     *            The element that is to be removed form the index.
     */
    private void removeIndex(E element) {
        indexMap.remove(indexFunction.apply(element));
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingCollection#clear()
     */
    @Override
    public void clear() {
        super.clear();
        indexMap.clear();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.google.common.collect.ForwardingCollection#remove(java.lang.Object)
     */
    @Override
    @SuppressWarnings(value = "unchecked")
    public boolean remove(Object object) {
        boolean removed = super.remove(object);
        if (removed) {
            removeIndex((E) object);
        }
        return removed;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.google.common.collect.ForwardingCollection#removeAll(java.util.Collection
     * )
     */
    @Override
    public boolean removeAll(Collection<?> collection) {
        boolean removed = super.removeAll(collection);
        if (removed) {
            rebuildIndex();
        }
        return removed;
    }

    /**
     * Clears the index and rebuilds it.
     */
    private void rebuildIndex() {
        indexMap.clear();
        indexElements(delegateList);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.google.common.collect.ForwardingCollection#retainAll(java.util.Collection
     * )
     */
    @Override
    public boolean retainAll(Collection<?> collection) {
        boolean changed = super.retainAll(collection);
        if (changed) {
            rebuildIndex();
        }
        return changed;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.google.common.collect.ForwardingList#remove(int)
     */
    @Override
    public E remove(int index) {
        E elementRemoved = super.remove(index);
        if (elementRemoved != null) {
            removeIndex(elementRemoved);
        }
        return elementRemoved;
    }

    /**
     * Retrieves an list element from the index for the specified key
     * 
     * @param key
     *            The key that will be used to retrieve the list element from
     *            the index.
     * @return The corresponding list element for the index.
     */
    public E getByIndex(K key) {
        return indexMap.get(key);
    }

    /**
     * Checks whether the key is present in the index.
     * 
     * @param key
     *            The key that is searched for in the index.
     * @return true, if the key exists, else false.
     */
    public boolean containsByIndex(K key) {
        return indexMap.containsKey(key);
    }

    /**
     * Returns the whole index as a {@link Map}. This map is a unmodifyable map, i.e. it
     * is read only.
     * 
     * @return A map with the current index.
     */
    public Map<K, E> getIndex() {
        return readOnlyIndexMap;
    }

    /**
     * A static factory as convenience method to easily create a
     * {@link UniqueIndexedList} for a {@link List}.
     * 
     * @param <E>
     *            The type of the list elements.
     * @param <K>
     *            The type of the key that is used for the index.
     * @param delegate
     *            The list all the calls are delegated to.
     * @param indexFunction
     *            The index function that is used to generate the keys for the
     *            index.
     * @return A new {@link UniqueIndexedList} that forwards all operations to
     *         the delegate list and maintains an index.
     */
    public static <E, K> UniqueIndexedList<E, K> create(List<E> delegate,
            Function<E, K> indexFunction) {
        return new UniqueIndexedList<E, K>(delegate, indexFunction);
    }

    /**
     * A static factory as convenience method to easily create a new
     * {@link UniqueIndexedList}.
     * 
     * @param <E>
     *            The type of the list elements.
     * @param <K>
     *            The type of the key that is used for the index.
     * @param indexFunction
     *            The index function that is used to generate the keys for the
     *            index.
     * @return A new {@link UniqueIndexedList} that internally is linked to a
     *         new empty {@link ArrayList} for which an index is maintained.
     */
    public static <E, K> UniqueIndexedList<E, K> create(
            Function<E, K> indexFunction) {
        List<E> newList = Lists.newArrayList();
        return new UniqueIndexedList<E, K>(newList, indexFunction);
    }

}
